package com.stars.easyms.base.util;

import org.springframework.util.StringUtils;

/**
 * <p>className: PatternMatcherUtil</p>
 * <p>description: 规则匹配工具类</p>
 *
 * @author guoguifang
 * @date 2019-08-29 13:34
 * @since 1.3.0
 */
public final class PatternMatcherUtil {

    private static final String PATH_SEPARATOR = "/";

    private static final char SINGLE_ASTERISK_CHAR = '*';

    private static final String SINGLE_ASTERISK = "*";

    private static final String DOUBLE_ASTERISK = "**";

    private static final String SLASH_ASTERISK = "/*";

    private static final String ASTERISK_DOT = "*.";

    private static final char SINGLE_SLASH = '/';

    private static final char SINGLE_DOT = '.';

    private static final char SINGLE_QUESTION_MARK = '?';

    public static boolean doMatch(String pattern, String path) {
        if (pattern == null) {
            return false;
        }

        if (!pattern.contains(SINGLE_ASTERISK)) {
            return pattern.equals(path);
        }

        // '*' is all
        if (pattern.equals(SINGLE_ASTERISK)) {
            return true;
        }

        // double asterisk to single asterisk
        while (pattern.contains(DOUBLE_ASTERISK)) {
            pattern = pattern.replaceAll("\\*\\*", SINGLE_ASTERISK);
        }
        pattern = pattern.replaceAll("\\.", "\\\\.");

        int start = 0;
        StringBuilder sb = new StringBuilder();
        int asteriskIdx = pattern.indexOf(SINGLE_ASTERISK_CHAR);
        while (asteriskIdx != -1) {
            String s = pattern.substring(start, asteriskIdx);
            sb.append(s).append(".*");
            start = asteriskIdx + 1;
            asteriskIdx = pattern.indexOf(SINGLE_ASTERISK_CHAR, start);
        }
        sb.append(pattern.substring(start));

        return path.matches(sb.toString());
    }

    /**
     * Actually match the given <code>path</code> against the given <code>pattern</code>. Servlet filter use this rule.
     *
     * @param pattern the pattern to match against
     * @param path    the path String to test
     * @return <code>true</code> if the supplied <code>path</code> matched, <code>false</code> if it didn't
     */
    public static boolean doRequestPathMatch(String pattern, String path) {
        if (pattern == null) {
            return false;
        }

        if (pattern.equals(path)) {
            return true;
        }

        if (SLASH_ASTERISK.equals(pattern)) {
            return true;
        }
        if (pattern.endsWith(SLASH_ASTERISK)) {
            if (pattern.regionMatches(0, path, 0, pattern.length() - 2)) {
                if (path.length() == (pattern.length() - 2)) {
                    return true;
                }
                return SINGLE_SLASH == path.charAt(pattern.length() - 2);
            }
            return false;
        }

        if (pattern.startsWith(ASTERISK_DOT)) {
            int slash = path.lastIndexOf(SINGLE_SLASH);
            int period = path.lastIndexOf(SINGLE_DOT);
            if ((slash >= 0) && (period > slash) && (period != path.length() - 1)
                    && ((path.length() - period) == (pattern.length() - 1))) {
                return pattern.regionMatches(2, path, period + 1, pattern.length() - 2);
            }
        }

        return false;
    }

    /**
     * Actually match the given <code>path</code> against the given <code>pattern</code>. Shiro auth filter use this rule.
     *
     * @param pattern   the pattern to match against
     * @param path      the path String to test
     * @param fullMatch whether a full pattern match is required
     *                  (else a pattern match as far as the given base path goes is sufficient)
     * @return <code>true</code> if the supplied <code>path</code> matched, <code>false</code> if it didn't
     */
    public static boolean doUrlPatternMatch(String pattern, String path, boolean fullMatch) {
        if (path.startsWith(PATH_SEPARATOR) != pattern.startsWith(PATH_SEPARATOR)) {
            return false;
        }

        String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, PATH_SEPARATOR);
        String[] pathDirs = StringUtils.tokenizeToStringArray(path, PATH_SEPARATOR);

        int pattIdxStart = 0;
        int pattIdxEnd = pattDirs.length - 1;
        int pathIdxStart = 0;
        int pathIdxEnd = pathDirs.length - 1;

        // Match all elements up to the first **
        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
            String patDir = pattDirs[pattIdxStart];
            if (DOUBLE_ASTERISK.equals(patDir)) {
                break;
            }
            if (!matchStrings(patDir, pathDirs[pathIdxStart])) {
                return false;
            }
            pattIdxStart++;
            pathIdxStart++;
        }

        if (pathIdxStart > pathIdxEnd) {
            // Path is exhausted, only match if rest of pattern is * or **'s
            if (pattIdxStart > pattIdxEnd) {
                return pattern.endsWith(PATH_SEPARATOR) == path.endsWith(PATH_SEPARATOR);
            }
            if (!fullMatch) {
                return true;
            }
            if (pattIdxStart == pattIdxEnd && "*".equals(pattDirs[pattIdxStart]) &&
                    path.endsWith(PATH_SEPARATOR)) {
                return true;
            }
            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
                if (!DOUBLE_ASTERISK.equals(pattDirs[i])) {
                    return false;
                }
            }
            return true;
        } else if (pattIdxStart > pattIdxEnd) {
            // String not exhausted, but pattern is. Failure.
            return false;
        } else if (!fullMatch && DOUBLE_ASTERISK.equals(pattDirs[pattIdxStart])) {
            // Path start definitely matches due to "**" part in pattern.
            return true;
        }

        // up to last '**'
        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
            String patDir = pattDirs[pattIdxEnd];
            if (DOUBLE_ASTERISK.equals(patDir)) {
                break;
            }
            if (!matchStrings(patDir, pathDirs[pathIdxEnd])) {
                return false;
            }
            pattIdxEnd--;
            pathIdxEnd--;
        }
        if (pathIdxStart > pathIdxEnd) {
            // String is exhausted
            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
                if (!DOUBLE_ASTERISK.equals(pattDirs[i])) {
                    return false;
                }
            }
            return true;
        }

        while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
            int patIdxTmp = -1;
            for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
                if (DOUBLE_ASTERISK.equals(pattDirs[i])) {
                    patIdxTmp = i;
                    break;
                }
            }
            if (patIdxTmp == pattIdxStart + 1) {
                // '**/**' situation, so skip one
                pattIdxStart++;
                continue;
            }
            // Find the pattern between padIdxStart & padIdxTmp in str between
            // strIdxStart & strIdxEnd
            int patLength = (patIdxTmp - pattIdxStart - 1);
            int strLength = (pathIdxEnd - pathIdxStart + 1);
            int foundIdx = -1;

            strLoop:
            for (int i = 0; i <= strLength - patLength; i++) {
                for (int j = 0; j < patLength; j++) {
                    String subPat = pattDirs[pattIdxStart + j + 1];
                    String subStr = pathDirs[pathIdxStart + i + j];
                    if (!matchStrings(subPat, subStr)) {
                        continue strLoop;
                    }
                }
                foundIdx = pathIdxStart + i;
                break;
            }

            if (foundIdx == -1) {
                return false;
            }

            pattIdxStart = patIdxTmp;
            pathIdxStart = foundIdx + patLength;
        }

        for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
            if (!DOUBLE_ASTERISK.equals(pattDirs[i])) {
                return false;
            }
        }

        return true;
    }

    /**
     * Tests whether or not a string matches against a pattern.
     * The pattern may contain two special characters:<br>
     * '*' means zero or more characters<br>
     * '?' means one and only one character
     *
     * @param pattern pattern to match against.
     *                Must not be <code>null</code>.
     * @param str     string which must be matched against the pattern.
     *                Must not be <code>null</code>.
     * @return <code>true</code> if the string matches against the
     * pattern, or <code>false</code> otherwise.
     */
    private static boolean matchStrings(String pattern, String str) {
        char[] patArr = pattern.toCharArray();
        char[] strArr = str.toCharArray();
        int patIdxStart = 0;
        int patIdxEnd = patArr.length - 1;
        int strIdxStart = 0;
        int strIdxEnd = strArr.length - 1;
        char ch;

        boolean containsStar = false;
        for (char aPatArr : patArr) {
            if (aPatArr == SINGLE_ASTERISK_CHAR) {
                containsStar = true;
                break;
            }
        }

        if (!containsStar) {
            // No '*'s, so we make a shortcut, Pattern and string do not have the same size
            if (patIdxEnd != strIdxEnd) {
                return false;
            }
            for (int i = 0; i <= patIdxEnd; i++) {
                ch = patArr[i];
                if (ch != SINGLE_QUESTION_MARK && ch != strArr[i]) {
                    return false;
                }
            }
            // String matches against pattern
            return true;
        }

        // Pattern contains only '*', which matches anything
        if (patIdxEnd == 0) {
            return true;
        }

        // Process characters before first star
        while ((ch = patArr[patIdxStart]) != SINGLE_ASTERISK_CHAR && strIdxStart <= strIdxEnd) {
            if (ch != SINGLE_QUESTION_MARK && ch != strArr[strIdxStart]) {
                return false;
            }
            patIdxStart++;
            strIdxStart++;
        }
        if (strIdxStart > strIdxEnd) {
            // All characters in the string are used. Check if only '*'s are
            // left in the pattern. If so, we succeeded. Otherwise failure.
            for (int i = patIdxStart; i <= patIdxEnd; i++) {
                if (patArr[i] != SINGLE_ASTERISK_CHAR) {
                    return false;
                }
            }
            return true;
        }

        // Process characters after last star
        while ((ch = patArr[patIdxEnd]) != SINGLE_ASTERISK_CHAR && strIdxStart <= strIdxEnd) {
            if (ch != SINGLE_QUESTION_MARK && ch != strArr[strIdxEnd]) {
                return false;
            }
            patIdxEnd--;
            strIdxEnd--;
        }
        if (strIdxStart > strIdxEnd) {
            // All characters in the string are used. Check if only '*'s are
            // left in the pattern. If so, we succeeded. Otherwise failure.
            for (int i = patIdxStart; i <= patIdxEnd; i++) {
                if (patArr[i] != SINGLE_ASTERISK_CHAR) {
                    return false;
                }
            }
            return true;
        }

        // process pattern between stars. padIdxStart and patIdxEnd point
        // always to a '*'.
        while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
            int patIdxTmp = -1;
            for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
                if (patArr[i] == SINGLE_ASTERISK_CHAR) {
                    patIdxTmp = i;
                    break;
                }
            }
            if (patIdxTmp == patIdxStart + 1) {
                // Two stars next to each other, skip the first one.
                patIdxStart++;
                continue;
            }
            // Find the pattern between padIdxStart & padIdxTmp in str between
            // strIdxStart & strIdxEnd
            int patLength = (patIdxTmp - patIdxStart - 1);
            int strLength = (strIdxEnd - strIdxStart + 1);
            int foundIdx = -1;

            strLoop:
            for (int i = 0; i <= strLength - patLength; i++) {
                for (int j = 0; j < patLength; j++) {
                    ch = patArr[patIdxStart + j + 1];
                    if (ch != SINGLE_QUESTION_MARK && ch != strArr[strIdxStart + i + j]) {
                        continue strLoop;
                    }
                }

                foundIdx = strIdxStart + i;
                break;
            }

            if (foundIdx == -1) {
                return false;
            }

            patIdxStart = patIdxTmp;
            strIdxStart = foundIdx + patLength;
        }

        // All characters in the string are used. Check if only '*'s are left
        // in the pattern. If so, we succeeded. Otherwise failure.
        for (int i = patIdxStart; i <= patIdxEnd; i++) {
            if (patArr[i] != SINGLE_ASTERISK_CHAR) {
                return false;
            }
        }

        return true;
    }

    private PatternMatcherUtil() {
    }
}